1 module hip.gui.widget; 2 3 /** 4 * Whenever implementing layouts, only modify worldTransform. 5 * Never modify local transform from other places. Only world transform is valid. 6 */ 7 class Widget 8 { 9 struct Bounds 10 { 11 int x, y, width, height; 12 } 13 struct Transform 14 { 15 int x, y; 16 float rotation = 0, scaleX = 1, scaleY = 1; 17 } 18 int width, height; 19 20 protected Widget parent; 21 protected Widget[] children; 22 protected Transform worldTransform; 23 protected Transform localTransform; 24 protected bool visible = true; 25 protected bool propagates = true; 26 protected bool isDirty = true; 27 28 29 Bounds getWorldBounds() 30 { 31 Bounds b = Bounds(worldTransform.x, worldTransform.y, width, height); 32 Bounds unmod = b; 33 import hip.api; 34 foreach(ch; children) 35 { 36 import hip.math.utils:min, max; 37 Bounds chBounds = ch.getWorldBounds; 38 b.x = min(b.x, chBounds.x); 39 b.y = min(b.y, chBounds.y); 40 b.width = max(b.width, chBounds.width+chBounds.x - unmod.x); 41 b.height = max(b.height, chBounds.height+chBounds.y - unmod.y); 42 } 43 return b; 44 } 45 46 Widget findWidgetAt(float[2] pos){return findWidgetAt(cast(int)pos[0], cast(int)pos[1]);} 47 Widget findWidgetAt(int x, int y) 48 { 49 import hip.math.collision; 50 foreach_reverse(w; children) 51 { 52 Bounds wb = w.getWorldBounds(); 53 if(w.visible && isPointInRect(x, y, wb.x, wb.y, wb.width, wb.height)) 54 { 55 if(w.propagates) 56 return w.findWidgetAt(x, y); 57 return w; 58 } 59 } 60 61 Bounds wb = getWorldBounds(); 62 return isPointInRect(x, y, wb.x, wb.y, wb.width, wb.height) ? this : null; 63 } 64 65 Bounds getLocalBounds(){return Bounds(localTransform.x,localTransform.y,width,height);} 66 67 void setPosition(int x, int y) 68 { 69 isDirty = true; 70 localTransform.x = x; 71 localTransform.y = y; 72 setChildrenDirty(); 73 } 74 75 protected void setChildrenDirty() 76 { 77 foreach(ch; children) 78 { 79 ch.isDirty = true; 80 ch.setChildrenDirty(); 81 } 82 } 83 84 private Widget getDirtyRoot() 85 { 86 Widget curr = parent; 87 Widget last = curr; 88 while(curr && curr.isDirty) 89 { 90 last = curr; 91 curr = curr.parent; 92 } 93 return curr is null ? last : curr; 94 } 95 96 private void updateWorldTransform(in Transform* parentTransform) 97 { 98 if(parentTransform is null) 99 worldTransform = localTransform; 100 else 101 { 102 alias p = parentTransform; 103 worldTransform.x = p.x+localTransform.x; 104 worldTransform.y = p.y+localTransform.y; 105 worldTransform.rotation = p.rotation+localTransform.rotation; 106 worldTransform.scaleX = p.scaleX*localTransform.scaleX; 107 worldTransform.scaleY = p.scaleY*localTransform.scaleY; 108 } 109 isDirty = false; 110 foreach(ch; children) 111 ch.updateWorldTransform(&worldTransform); 112 } 113 private void recalculateWorld() 114 { 115 if(isDirty) 116 { 117 Widget root = getDirtyRoot(); 118 if(root) 119 root.updateWorldTransform(root.parent ? &root.parent.worldTransform : null); 120 else 121 updateWorldTransform(parent ? &parent.worldTransform : null); 122 } 123 } 124 125 void addChild(scope Widget[] widgets...) 126 { 127 foreach(w; widgets) addChild(w); 128 } 129 void addChild(Widget w) 130 { 131 children~= w; 132 w.isDirty = true; 133 w.parent = this; 134 w.setChildrenDirty(); 135 } 136 137 void removeChild(Widget child) 138 { 139 import hip.util.array; 140 if(!remove(children, child)) 141 throw new Exception("Doesn't contain child."); 142 child.parent = null; 143 setChildrenDirty(); 144 } 145 146 void setParent(Widget w) 147 { 148 w.addChild(this); 149 } 150 151 //Event Methods 152 void onFocusEnter() 153 { 154 isFocused = true; 155 } 156 void onFocusExit() 157 { 158 isFocused = false; 159 } 160 161 void onScroll(float[3] currentScroll, float[3] lastScroll) 162 { 163 setPosition( 164 cast(int)(localTransform.x + currentScroll[0] - lastScroll[0]), 165 cast(int)(localTransform.y + currentScroll[1] - lastScroll[1]) 166 ); 167 } 168 ///Executed the first time the mouse enters in the widget's boundaries 169 void onMouseEnter(){} 170 ///Executed when the mouse goes down inside the widget 171 void onMouseDown(){} 172 ///Executed when both a mousedown and mouseup is executed when mouse is over this widget 173 void onMouseClick(){} 174 ///If onMouseDown was executed, onMouseUp will be called even if the mouse is not inside the widget 175 void onMouseUp(){} 176 void onMouseMove(){} 177 private int dragOffsetX, dragOffsetY; 178 void onDragStart(int x, int y) 179 { 180 dragOffsetX = worldTransform.x - x; 181 dragOffsetY = worldTransform.y - y; 182 } 183 void onDragged(int x, int y) 184 { 185 import hip.api; 186 setPosition(x + dragOffsetX, y + dragOffsetY); 187 } 188 void onDragEnd(){} 189 ///Returns whether it accepted the receive 190 bool onDropReceived(Widget w){return false;} 191 void onMouseExit(){} 192 bool isDraggable; 193 bool isFocused; 194 //End Event Methods 195 196 197 void update() 198 { 199 foreach(ch; children) ch.update(); 200 } 201 202 protected void preRender(){recalculateWorld();} 203 protected void render() 204 { 205 preRender(); 206 onRender(); 207 foreach(ch; children) 208 if(ch.visible) ch.render(); 209 } 210 abstract void onRender(); 211 } 212 213 interface IWidgetRenderer 214 { 215 void render(int x, int y, int width, int height); 216 } 217 218 class DebugWidgetRenderer : IWidgetRenderer 219 { 220 import hip.api.graphics.color; 221 import hip.math.random; 222 HipColor color; 223 this() 224 { 225 color[] = Random.rangeub(0, 255); 226 } 227 this(HipColor color){this.color = color;} 228 229 void render(int x, int y, int width, int height) 230 { 231 import hip.api.graphics.g2d.renderer2d; 232 fillRoundRect(x,y,width,height, 4, color); 233 } 234 }